﻿using System;
using System.IO;
using Moonlite.ExtensionApi;

namespace Moonlite.Data.Configuration
{
    /// <summary>
    /// Represents an options storer that stores options in the AppData directory using a binary format.
    /// </summary>
    internal class AppDataOptionsStorer : IOptionsStorer
    {
        static readonly byte[] HeaderBytes = new byte[] { 0x4D, 0x44, 0x43 };
        static readonly byte AssemblyPathByte = 0x41;
        static readonly byte TypeByte = 0x54;
        static readonly byte StoragePathByte = 0x53;

        /// <summary>
        /// Gets the AppData directory path.
        /// </summary>
        /// <returns>The AppData directory path.</returns>
        private static string GetAppDataDirectoryPath()
        {
            return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        }

        /// <summary>
        /// Gets the storage file path.
        /// </summary>
        /// <returns>The storage file path.</returns>
        private static string GetStorageFilePath()
        {
            return GetAppDataDirectoryPath() + "\\Moonlite.Data.Configuration\\InternalSettingsStorage.datacfg";
        }

        /// <summary>
        /// Saves an <see cref="AssemblyConfigurationAttachment"/> to the specified <see cref="System.IO.BinaryWriter"/>.
        /// </summary>
        /// <param name="attachment">The <see cref="AssemblyConfigurationAttachment"/>.</param>
        /// <param name="writer">The <see cref="System.IO.BinaryWriter"/>.</param>
        private static void SaveAttachment(AssemblyConfigurationAttachment attachment, BinaryWriter writer)
        {
            writer.Write(AssemblyPathByte);
            writer.Write(AssemblyPathHelpers.GetAssemblyPath(attachment.Assembly));
            writer.Write(TypeByte);
            writer.Write(attachment.ConfigurationInstanceType.FullName);
            writer.Write(StoragePathByte);
            writer.Write(attachment.ConfigurationStoragePath);
        }

        /// <summary>
        /// Loads an <see cref="AssemblyConfigurationAttachment"/> from the specified <see cref="System.IO.BinaryReader"/>.
        /// </summary>
        /// <param name="reader">The <see cref="System.IO.BinaryReader"/>.</param>
        /// <param name="exit">Whether the source of the <paramref name="reader"/> should exit its loop.</param>
        /// <returns>The loaded <see cref="AssemblyConfigurationAttachment"/>.</returns>
        private static AssemblyConfigurationAttachment LoadAttachment(BinaryReader reader, out bool exit)
        {
            if (reader.PeekChar() == -1)
            {
                exit = true;
                return null;
            }

            string assemblyPath = "";
            if (reader.ReadByte() == AssemblyPathByte)
            {
                assemblyPath = reader.ReadString();
            }
            else
            {
                exit = true;
                return null;
            }

            Type type = null;
            if (reader.ReadByte() == TypeByte)
            {
                type = TypeGetHelpers.GetTypeFromFullName(reader.ReadString());
            }
            else
            {
                exit = true;
                return null;
            }

            string storagePath = "";
            if (reader.ReadByte() == StoragePathByte)
            {
                storagePath = reader.ReadString();
            }
            else
            {
                exit = true;
                return null;
            }

            if (assemblyPath != "" && type != null && storagePath != "")
            {
                AssemblyConfigurationAttachment attachment = new AssemblyConfigurationAttachment(assemblyPath, storagePath, type);
                exit = false;
                return attachment;
            }
            else
            {
                exit = true;
                return null;
            }
        }

        /// <summary>
        /// Writes the AppDataOptionsStorer file format header to the specified <see cref="System.IO.BinaryWriter"/>.
        /// </summary>
        /// <param name="writer">The <see cref="System.IO.BinaryWriter"/>.</param>
        private static void WriteHeader(BinaryWriter writer)
        {
            writer.Write(HeaderBytes);
        }

        /// <summary>
        /// Checks the header of the given <see cref="System.IO.BinaryReader"/>.
        /// </summary>
        /// <param name="reader">The <see cref="System.IO.BinaryReader"/>.</param>
        /// <returns>Whether the <see cref="System.IO.BinaryReader"/> had a correct header.</returns>
        private static bool CheckHeader(BinaryReader reader)
        {
            byte[] header = reader.ReadBytes(HeaderBytes.Length);
            bool correctHeader = true;
            for (int i = 0; i < header.Length; i++)
            {
                if (header[i] == HeaderBytes[i])
                {
                    continue;
                }
                else
                {
                    correctHeader = false;
                    break;
                }
            }

            return correctHeader;
        }

        /// <summary>
        /// Saves the options.
        /// </summary>
        public void Save()
        {
            string storageFilePath = GetStorageFilePath();
            try
            {
                DirectoryHelper.CreatePath(storageFilePath);
                FileStream stream = new FileStream(storageFilePath, FileMode.Create);
                BinaryWriter writer = new BinaryWriter(stream);
                WriteHeader(writer);
                foreach (AssemblyConfigurationAttachment attachment in AttachmentRegister.Pairs)
                {
                    SaveAttachment(attachment, writer);
                }

                writer.Dispose();
            }
            catch (Exception)
            {
                throw;
            }
        }

        /// <summary>
        /// Loads the options.
        /// </summary>
        public void Load()
        {
            string storageFilePath = GetStorageFilePath();
            try
            {
                DirectoryHelper.CreatePath(storageFilePath);
                if (!File.Exists(storageFilePath))
                {
                    Save();
                }

                FileStream stream = new FileStream(storageFilePath, FileMode.Open);
                BinaryReader reader = new BinaryReader(stream);
                if (CheckHeader(reader))
                {
                    while (true)
                    {
                        bool exit = false;
                        AssemblyConfigurationAttachment attachment = LoadAttachment(reader, out exit);
                        if (exit)
                        {
                            break;
                        }

                        AttachmentRegister.RegisterPair(attachment);
                    }
                }
                else
                {
                    ExceptionHelper.ThrowAppDataOptionsStorerWrongFileFormatException();
                }

                reader.Dispose();
            }
            catch (Exception)
            {
                throw;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="AppDataOptionsStorer"/> class.
        /// </summary>
        public AppDataOptionsStorer()
        {
        }
    }
}
